#ifndef SYNTAXCHECK_H
#define SYNTAXCHECK_H

#include "syntaxException.h"
#include "semanticException.h"
#include "typecheck.h"
#include <vector>
#include <string>
#include <array>
#include <utility>

using namespace std;

class Token
{
private:

    /*PROPERTIES*/
    string text;
    int classification;
    int lineNumber;

public:
    /*VALUES*/
    const static array<string, 14> keywords;
    const static array<string, 2> booleanValues;
    const static string additionOperator;
    const static string multiplicationOperator;
    const static string accChars;
    const static string specialChars;
    const static array<string, 3> types;

    Token() : Token (string("")) {};

    Token(const Token &that)
    {
        text = that.text;
        classification = that.classification;
        lineNumber = that.lineNumber;
    }

    explicit Token(const string &text) : Token (text, Classifications::UNCLASSIFIED) {}

    Token(const string &text, int classification) : Token (text, classification, 0) {}

    Token (string t, int c, int l) : text(move(t)), classification(c), lineNumber(l) {}

    string toString()
    {
        string ret = "TOKEN: " + text + "\n"
                          + "CLASSIFICATION: " + getClassificationName() + "\n"
                          + "LINE: " + to_string(lineNumber);
        return ret;
    }
    string & getText() {return this->text;}
    int getLineNumber() {return this->lineNumber;}

    /**
     * All possible classifications for tokens in Pascal
     */
    enum Classifications
    {
        KEYWORD = 1, //any word in keywords list
        IDENTIFIER = 2, //[a..z] ([a..z]|[0..9]|'_')*
        INTEGER = 3, //[0..9]+
        REAL = 4, //[0..9]*.[0..9]+
        BOOLEAN = 5, //true false
        COMPLEX = 6, //[REAL|INTEGER]i[+|-]?[REAL|INTEGER]
        DELIMITER = 7, //,;:.()
        ASSIGNMENT = 8, //:=
        RELATIONAL = 9, //<,>,=,<>,<=,>=
        ADDITION = 10, //or + -
        MULTIPLICATION = 11, //and * /
        UNCLASSIFIED = 12 //default value
    };

    int getClassification()
    {
        return classification;
    }

    string getClassificationName()
    {
        switch(classification)
        {
            case 1: return "keyword";
            case 2: return "identifier";
            case 3: return "integer";
            case 4: return "real";
            case 5: return "boolean";
            case 6: return "complex";
            case 7: return "delimiter";
            case 8: return "assignment";
            case 9: return "relational";
            case 10: return "addition";
            case 11: return "multiplication";
            case 12: return "unclassified";
            default: return "unclassified";
        }
    }
};

const array<string, 14> Token::keywords = {"program", "var", "integer", "real", "boolean", "procedure", "begin", "end", "if", "then", "else", "while", "do", "not"};
const array<string, 2> Token::booleanValues = {"true", "false"};
const string Token::additionOperator = "or";
const string Token::multiplicationOperator = "and";
const string Token::accChars = "_.:;,<>=+-/*(){}\' \t\n";
const string Token::specialChars = ":;,<>=+-/*()";
const array<string, 3> Token::types = {"integer", "real", "boolean"};

class SymbolsTable
{
    string MARK = "$";
    string MARK_TYPE = "markType";
    vector<Symbol> stack;
    string vd = "void";

public:

    SymbolsTable() = default;

    SymbolsTable(const SymbolsTable &that)
    {
        stack = that.stack;
    }

    string & getType(const string &identifierName)
    {
        auto i = (stack.size() - 1);
        while(i >= 0) {
            if(stack.at(i).getName() == (identifierName)) return stack.at(i).getType();
            i--;
        }

        return vd;
    }

    void enterScope()
    {
        stack.emplace_back(MARK, MARK_TYPE);
    }

    void exitScope() {
        auto i = stack.size() - 1;
        while(stack.at(i).getName() != (MARK)) {
            stack.erase(stack.begin() + i);
            i--;
        }

        stack.erase(stack.begin() + i);
    }

    bool searchDuplicateDeclaration(const string &identifierName) {

        auto i = stack.size() - 1;
        while(stack.at(i).getName() != (MARK)) {
            if(stack.at(i).getName() == (identifierName)) return true;
            i--;
        }

        return false;
    }

    bool searchIdentifier(const string &identifierName) {
        auto i = stack.size() - 1;
        while(i >= 0) {
            if(stack.at(i).getName() == (identifierName)) return true;
            i--;
        }

        return false;
    }

    void addSymbol(const string &text, const string &type)
    {
        stack.emplace_back(text, type);
    }

    void assignType(string type) {
        auto i = stack.size() - 1;

        while(stack.at(i).getType() == "void") {
            stack.at(i).setType(type);
            i--;
        }
    }

    void assignParameters()
    {
        Symbol procedureSymbol = getLastProcedure();
        auto i = stack.size() - 1;

        while(stack.at(i).getType() != ("procedure")) {
            if(stack.at(i).getType() != (MARK_TYPE)) procedureSymbol.addParameter(stack.at(i));
            i--;
        }

    }

    Symbol & getLastProcedure()
    {

        auto i = stack.size() - 1;

        while(stack.at(i).getType() != ("procedure"))
        {
            i--;
        }
        return stack.at(i);
    }

    Symbol & getProcedure(string &procedureName) {
        auto i = stack.size() - 1;
        while(stack.at(i).getName() != (procedureName) && stack.at(i).getType() == ("procedure") )
        {
            i--;
        }

        return stack.at(i);
    }

    string getProgramName()
    {
        return stack.at(1).getName();
    }

};

class SyntaxCheck
{
private:
    /*PROPERTIES*/
    vector<Token> tokens;
    Token* currentToken = nullptr;
    unsigned int count{};
    bool DEBUG_MODE = false;

    /*SEMANTIC ANALYZER*/
    SymbolsTable symbolsTable;
    TypeControl typeControl;

public:
    SyntaxCheck(vector<Token> &t, bool D) : tokens(t), DEBUG_MODE(D) {}

    SyntaxCheck (const SyntaxCheck &that)
    {
        tokens = that.tokens;
        currentToken = that.currentToken;
        count = that.count;
        DEBUG_MODE = that.DEBUG_MODE;

        symbolsTable = that.symbolsTable;
        typeControl = that.typeControl;
    }

    explicit SyntaxCheck(vector<Token> &tokens) : SyntaxCheck(tokens, true) {}

    SyntaxCheck() = default;

    void run() throw();

private:
    void program()throw();
    void varDeclaration()throw();
    void varDeclarationListA()throw();
    void varDeclarationListB()throw();
    void identifiersListA()throw();
    void identifiersListB()throw();
    void type()throw();
    void subProgramsDeclaration()throw();
    void subProgram()throw();
    void arguments()throw();
    void parameterListA()throw();
    void parameterListB()throw();
    void compoundCommand()throw();
    void optionalCommands()throw();
    void commandListA()throw();
    void commandListB()throw();
    void command()throw();
    void commandStructure()throw();
    void elsePart()throw();
    void procedureActivationA()throw();
    void expressionListA()throw();
    void expressionListB()throw();
    void expression()throw();
    void simpleExpressionA()throw();
    void simpleExpressionB()throw();
    void termA()throw();
    void termB()throw();
    void factor() throw();

    void getNextToken()
    {
        count++;
        if (count >= tokens.size())
        {
            count = static_cast<unsigned int>(tokens.size() - 1);
            syntaxError("No more tokens to be read.");
        }
        if (this->DEBUG_MODE)
        {
            cout << "Reading token: " << tokens.at(count).getText() << endl;
        }
        currentToken = &tokens.at(count);
    }

    bool containsType(const string &word) const
    {
        for(const string &type : Token::types)
        {
            if(word == type) return true;
        }

        return false;
    }

    void syntaxError(const string &errorMsg) throw()
    {
        string str = "Syntax Error! Line " + to_string(currentToken->getLineNumber()) + ":\n"
                          + currentToken->getText() + "\n" + errorMsg;
        throw SyntaxException(str);
    }

    void semanticError(const string &errorMsg) throw()
    {
        string str = "Semantic Error! Line " + to_string(currentToken->getLineNumber()) + ":\n" +
                currentToken->getText() + "\n" + errorMsg;
        throw SemanticException(str);
    }
};
#endif
